package cn.ddiancan.gateway.service;

import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

import cn.ddiancan.gateway.config.SsoConstants;
import cn.ddiancan.xddcloud.common.constants.XddHeaderConstant;
import cn.ddiancan.xddcloud.common.entity.CommonResult;
import cn.ddiancan.xddcloud.common.entity.UserVO;
import cn.ddiancan.xddcloud.common.security.JwtService;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;

@Service
@RequiredArgsConstructor
@Slf4j
public class AuthenticationService {

    private static final String USER_CACHE = "xddcloud:" + XddHeaderConstant.PERMISSION_CACHE_PREFIX + "gateway:user:";

    private final HttpClient httpClient;

    private final StringRedisTemplate redisTemplate;

    private final JwtService jwtService;

    @Getter
    private static final Cache<String, String> levelOneCache =
        CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(30, TimeUnit.MINUTES).build();

    @Value("${xddcloud.gateway.login.validate:}")
    private String ssoTokenValidate;

    @Value("${xddcloud.gateway.session.expire:30}")
    private String cacheExpire;

    @Value("${xddcloud.gateway.auth.enabled:true}")
    private Boolean authSwitch;

    private Boolean isSchedulerRequest(ServerWebExchange exchange) {
        return exchange.getRequest().getURI().getRawPath().contains("/publicservices/") ||
            exchange.getRequest().getURI().getRawPath().contains("/ischedule/");
    }

    public Mono<Boolean> doLogin(ServerWebExchange exchange) {
        if (isSchedulerRequest(exchange)) {
            UserVO userVO = new UserVO();
            userVO.setUserAccount("scheduler");
            userVO.setUserName("scheduler");
            userVO.setUserId("-1");
            return getAuthByUserInfo(Mono.just(userVO), "xddcloud.gateway:scheduler", exchange);
        }
        return doSsologin(exchange);
    }

    private Mono<Boolean> doSsologin(ServerWebExchange exchange) {
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        ServerHttpRequest request = exchange.getRequest();
        String accessToken = request.getHeaders().getFirst(SsoConstants.X_ACCESS_TOKEN);
        String userCode = request.getHeaders().getFirst(SsoConstants.USER_CODE);
        if (!StringUtils.hasText(accessToken) && !StringUtils.hasText(userCode)) {
            return Mono.just(false);
        }
        accessToken = StringUtils.hasText(accessToken) ? accessToken : userCode;
        // 两级缓存获取jwtToken
        UserVO userInfo = null;
        String jwtToken = StringUtils.hasText(levelOneCache.getIfPresent(accessToken)) ? levelOneCache.getIfPresent(
            accessToken) : redisTemplate.opsForValue().get(USER_CACHE + accessToken);
        if (StringUtils.hasText(jwtToken)) {
            userInfo = jwtService.getUserInfo(jwtToken);
        }
        Mono<UserVO> ddcUserMono = Objects.nonNull(userInfo) ? Mono.just(userInfo) : validateToken(exchange);
        return getAuthByUserInfo(ddcUserMono, accessToken, exchange);
    }

    private Mono<Boolean> getAuthByUserInfo(Mono<UserVO> userInfo, String storeKey, ServerWebExchange exchange) {
        return userInfo.flatMap(ddcUser -> {
            if (Objects.nonNull(ddcUser) && StringUtils.hasText(ddcUser.getUserName())) {
                String jwtServiceToken = jwtService.createToken(ddcUser);
                levelOneCache.put(storeKey, jwtServiceToken);
                redisTemplate.opsForValue()
                    .set(USER_CACHE + storeKey, jwtServiceToken, Long.parseLong(cacheExpire), TimeUnit.MINUTES);
                exchange.getAttributes().put(SsoConstants.JWT_TOKEN, jwtServiceToken);
                exchange.getAttributes().put(SsoConstants.UATH_TOKEN,
                    StringUtils.hasText(fetchSessionId(exchange)) ? fetchSessionId(exchange) : storeKey);
                levelOneCache.put(ddcUser.getUserAccount(), storeKey);
                return Mono.just(Boolean.TRUE);
            }
            return Mono.just(Boolean.FALSE);
        }).switchIfEmpty(Mono.just(Boolean.FALSE));
    }

    private String fetchSessionId(ServerWebExchange exchange) {
        ServerHttpRequest request = exchange.getRequest();
        String sessionId = request.getHeaders().getFirst(SsoConstants.USER_SESSION_ID);
        if (!StringUtils.hasText(sessionId)) {
            HttpCookie sessionInfo = request.getCookies().getFirst(SsoConstants.USER_SESSION_ID);
            if (Objects.nonNull(sessionInfo)) {
                sessionId = sessionInfo.getValue();
            }
        }
        return sessionId;
    }


    private Mono<UserVO> validateToken(ServerWebExchange exchange) {
        try {
            ServerHttpRequest request = exchange.getRequest();
            HttpHeaders headers = request.getHeaders();
            final DefaultHttpHeaders httpHeaders = new DefaultHttpHeaders();
            headers.forEach(httpHeaders::set);
            return httpClient.headers(header -> header.add(httpHeaders))
                .request(io.netty.handler.codec.http.HttpMethod.GET).uri(ssoTokenValidate)
                .responseSingle((response, reciever) -> reciever.asString().map(AuthenticationService::apply));
        } catch (Exception e) {
            log.error(e.getMessage());
        }
        return Mono.empty();
    }

    private static UserVO apply(String res) {
        log.info("认证返回===================={}", res);
        CommonResult<UserVO> resultInfo = JSON.parseObject(res, new TypeReference<>() {
        });
        return Optional.ofNullable(resultInfo.getData()).orElse(new UserVO());
    }
}
